// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)

package org.xbill.DNS;

import java.io.*;
import java.text.*;
import java.util.*;
import org.xbill.DNS.utils.*;

/**
 * A generic DNS resource record. The specific record types extend this class. A
 * record contains a name, type, class, ttl, and rdata.
 * 
 * @author Brian Wellington
 */

public abstract class Record implements Cloneable, Comparable, Serializable {

	private static final long serialVersionUID = 2694906050116005466L;

	protected Name name;
	protected int type, dclass;
	protected long ttl;

	private static final DecimalFormat byteFormat = new DecimalFormat();

	static {
		byteFormat.setMinimumIntegerDigits(3);
	}

	protected Record() {
	}

	Record(Name name, int type, int dclass, long ttl) {
		if (!name.isAbsolute())
			throw new RelativeNameException(name);
		Type.check(type);
		DClass.check(dclass);
		TTL.check(ttl);
		this.name = name;
		this.type = type;
		this.dclass = dclass;
		this.ttl = ttl;
	}

	/**
	 * Creates an empty record of the correct type; must be overriden
	 */
	abstract Record getObject();

	private static final Record getEmptyRecord(Name name, int type, int dclass,
			long ttl, boolean hasData) {
		Record proto, rec;

		if (hasData) {
			proto = Type.getProto(type);
			if (proto != null)
				rec = proto.getObject();
			else
				rec = new UNKRecord();
		} else
			rec = new EmptyRecord();
		rec.name = name;
		rec.type = type;
		rec.dclass = dclass;
		rec.ttl = ttl;
		return rec;
	}

	/**
	 * Converts the type-specific RR to wire format - must be overriden
	 */
	abstract void rrFromWire(DNSInput in) throws IOException;

	private static Record newRecord(Name name, int type, int dclass, long ttl,
			int length, DNSInput in) throws IOException {
		Record rec;
		rec = getEmptyRecord(name, type, dclass, ttl, in != null);
		if (in != null) {
			if (in.remaining() < length)
				throw new WireParseException("truncated record");
			in.setActive(length);

			rec.rrFromWire(in);

			if (in.remaining() > 0)
				throw new WireParseException("invalid record length");
			in.clearActive();
		}
		return rec;
	}

	/**
	 * Creates a new record, with the given parameters.
	 * 
	 * @param name
	 *            The owner name of the record.
	 * @param type
	 *            The record's type.
	 * @param dclass
	 *            The record's class.
	 * @param ttl
	 *            The record's time to live.
	 * @param length
	 *            The length of the record's data.
	 * @param data
	 *            The rdata of the record, in uncompressed DNS wire format. Only
	 *            the first length bytes are used.
	 */
	public static Record newRecord(Name name, int type, int dclass, long ttl,
			int length, byte[] data) {
		if (!name.isAbsolute())
			throw new RelativeNameException(name);
		Type.check(type);
		DClass.check(dclass);
		TTL.check(ttl);

		DNSInput in;
		if (data != null)
			in = new DNSInput(data);
		else
			in = null;
		try {
			return newRecord(name, type, dclass, ttl, length, in);
		} catch (IOException e) {
			return null;
		}
	}

	/**
	 * Creates a new record, with the given parameters.
	 * 
	 * @param name
	 *            The owner name of the record.
	 * @param type
	 *            The record's type.
	 * @param dclass
	 *            The record's class.
	 * @param ttl
	 *            The record's time to live.
	 * @param data
	 *            The complete rdata of the record, in uncompressed DNS wire
	 *            format.
	 */
	public static Record newRecord(Name name, int type, int dclass, long ttl,
			byte[] data) {
		return newRecord(name, type, dclass, ttl, data.length, data);
	}

	/**
	 * Creates a new empty record, with the given parameters.
	 * 
	 * @param name
	 *            The owner name of the record.
	 * @param type
	 *            The record's type.
	 * @param dclass
	 *            The record's class.
	 * @param ttl
	 *            The record's time to live.
	 * @return An object of a subclass of Record
	 */
	public static Record newRecord(Name name, int type, int dclass, long ttl) {
		if (!name.isAbsolute())
			throw new RelativeNameException(name);
		Type.check(type);
		DClass.check(dclass);
		TTL.check(ttl);

		return getEmptyRecord(name, type, dclass, ttl, false);
	}

	/**
	 * Creates a new empty record, with the given parameters. This method is
	 * designed to create records that will be added to the QUERY section of a
	 * message.
	 * 
	 * @param name
	 *            The owner name of the record.
	 * @param type
	 *            The record's type.
	 * @param dclass
	 *            The record's class.
	 * @return An object of a subclass of Record
	 */
	public static Record newRecord(Name name, int type, int dclass) {
		return newRecord(name, type, dclass, 0);
	}

	static Record fromWire(DNSInput in, int section, boolean isUpdate)
			throws IOException {
		int type, dclass;
		long ttl;
		int length;
		Name name;
		Record rec;

		name = new Name(in);
		type = in.readU16();
		dclass = in.readU16();

		if (section == Section.QUESTION)
			return newRecord(name, type, dclass);

		ttl = in.readU32();
		length = in.readU16();
		if (length == 0 && isUpdate
				&& (section == Section.PREREQ || section == Section.UPDATE))
			return newRecord(name, type, dclass, ttl);
		rec = newRecord(name, type, dclass, ttl, length, in);
		return rec;
	}

	static Record fromWire(DNSInput in, int section) throws IOException {
		return fromWire(in, section, false);
	}

	/**
	 * Builds a Record from DNS uncompressed wire format.
	 */
	public static Record fromWire(byte[] b, int section) throws IOException {
		return fromWire(new DNSInput(b), section, false);
	}

	void toWire(DNSOutput out, int section, Compression c) {
		name.toWire(out, c);
		out.writeU16(type);
		out.writeU16(dclass);
		if (section == Section.QUESTION)
			return;
		out.writeU32(ttl);
		int lengthPosition = out.current();
		out.writeU16(0); /* until we know better */
		rrToWire(out, c, false);
		int rrlength = out.current() - lengthPosition - 2;
		out.writeU16At(rrlength, lengthPosition);
	}

	/**
	 * Converts a Record into DNS uncompressed wire format.
	 */
	public byte[] toWire(int section) {
		DNSOutput out = new DNSOutput();
		toWire(out, section, null);
		return out.toByteArray();
	}

	private void toWireCanonical(DNSOutput out, boolean noTTL) {
		name.toWireCanonical(out);
		out.writeU16(type);
		out.writeU16(dclass);
		if (noTTL) {
			out.writeU32(0);
		} else {
			out.writeU32(ttl);
		}
		int lengthPosition = out.current();
		out.writeU16(0); /* until we know better */
		rrToWire(out, null, true);
		int rrlength = out.current() - lengthPosition - 2;
		out.writeU16At(rrlength, lengthPosition);
	}

	/*
	 * Converts a Record into canonical DNS uncompressed wire format (all names
	 * are converted to lowercase), optionally ignoring the TTL.
	 */
	private byte[] toWireCanonical(boolean noTTL) {
		DNSOutput out = new DNSOutput();
		toWireCanonical(out, noTTL);
		return out.toByteArray();
	}

	/**
	 * Converts a Record into canonical DNS uncompressed wire format (all names
	 * are converted to lowercase).
	 */
	public byte[] toWireCanonical() {
		return toWireCanonical(false);
	}

	/**
	 * Converts the rdata in a Record into canonical DNS uncompressed wire
	 * format (all names are converted to lowercase).
	 */
	public byte[] rdataToWireCanonical() {
		DNSOutput out = new DNSOutput();
		rrToWire(out, null, true);
		return out.toByteArray();
	}

	/**
	 * Converts the type-specific RR to text format - must be overriden
	 */
	abstract String rrToString();

	/**
	 * Converts the rdata portion of a Record into a String representation
	 */
	public String rdataToString() {
		return rrToString();
	}

	/**
	 * Converts a Record into a String representation
	 */
	public String toString() {
		StringBuffer sb = new StringBuffer();
		sb.append(name);
		if (sb.length() < 8)
			sb.append("\t");
		if (sb.length() < 16)
			sb.append("\t");
		sb.append("\t");
		if (Options.check("BINDTTL"))
			sb.append(TTL.format(ttl));
		else
			sb.append(ttl);
		sb.append("\t");
		if (dclass != DClass.IN || !Options.check("noPrintIN")) {
			sb.append(DClass.string(dclass));
			sb.append("\t");
		}
		sb.append(Type.string(type));
		String rdata = rrToString();
		if (!rdata.equals("")) {
			sb.append("\t");
			sb.append(rdata);
		}
		return sb.toString();
	}

	/**
	 * Converts the text format of an RR to the internal format - must be
	 * overriden
	 */
	abstract void rdataFromString(Tokenizer st, Name origin) throws IOException;

	/**
	 * Converts a String into a byte array.
	 */
	protected static byte[] byteArrayFromString(String s)
			throws TextParseException {
		byte[] array = s.getBytes();
		boolean escaped = false;
		boolean hasEscapes = false;

		for (int i = 0; i < array.length; i++) {
			if (array[i] == '\\') {
				hasEscapes = true;
				break;
			}
		}
		if (!hasEscapes) {
			if (array.length > 255) {
				throw new TextParseException("text string too long");
			}
			return array;
		}

		ByteArrayOutputStream os = new ByteArrayOutputStream();

		int digits = 0;
		int intval = 0;
		for (int i = 0; i < array.length; i++) {
			byte b = array[i];
			if (escaped) {
				if (b >= '0' && b <= '9' && digits < 3) {
					digits++;
					intval *= 10;
					intval += (b - '0');
					if (intval > 255)
						throw new TextParseException("bad escape");
					if (digits < 3)
						continue;
					b = (byte) intval;
				} else if (digits > 0 && digits < 3)
					throw new TextParseException("bad escape");
				os.write(b);
				escaped = false;
			} else if (array[i] == '\\') {
				escaped = true;
				digits = 0;
				intval = 0;
			} else
				os.write(array[i]);
		}
		if (digits > 0 && digits < 3)
			throw new TextParseException("bad escape");
		array = os.toByteArray();
		if (array.length > 255) {
			throw new TextParseException("text string too long");
		}

		return os.toByteArray();
	}

	/**
	 * Converts a byte array into a String.
	 */
	protected static String byteArrayToString(byte[] array, boolean quote) {
		StringBuffer sb = new StringBuffer();
		if (quote)
			sb.append('"');
		for (int i = 0; i < array.length; i++) {
			int b = array[i] & 0xFF;
			if (b < 0x20 || b >= 0x7f) {
				sb.append('\\');
				sb.append(byteFormat.format(b));
			} else if (b == '"' || b == '\\') {
				sb.append('\\');
				sb.append((char) b);
			} else
				sb.append((char) b);
		}
		if (quote)
			sb.append('"');
		return sb.toString();
	}

	/**
	 * Converts a byte array into the unknown RR format.
	 */
	protected static String unknownToString(byte[] data) {
		StringBuffer sb = new StringBuffer();
		sb.append("\\# ");
		sb.append(data.length);
		sb.append(" ");
		sb.append(base16.toString(data));
		return sb.toString();
	}

	/**
	 * Builds a new Record from its textual representation
	 * 
	 * @param name
	 *            The owner name of the record.
	 * @param type
	 *            The record's type.
	 * @param dclass
	 *            The record's class.
	 * @param ttl
	 *            The record's time to live.
	 * @param st
	 *            A tokenizer containing the textual representation of the
	 *            rdata.
	 * @param origin
	 *            The default origin to be appended to relative domain names.
	 * @return The new record
	 * @throws IOException
	 *             The text format was invalid.
	 */
	public static Record fromString(Name name, int type, int dclass, long ttl,
			Tokenizer st, Name origin) throws IOException {
		Record rec;

		if (!name.isAbsolute())
			throw new RelativeNameException(name);
		Type.check(type);
		DClass.check(dclass);
		TTL.check(ttl);

		Tokenizer.Token t = st.get();
		if (t.type == Tokenizer.IDENTIFIER && t.value.equals("\\#")) {
			int length = st.getUInt16();
			byte[] data = st.getHex();
			if (data == null) {
				data = new byte[0];
			}
			if (length != data.length)
				throw st.exception("invalid unknown RR encoding: "
						+ "length mismatch");
			DNSInput in = new DNSInput(data);
			return newRecord(name, type, dclass, ttl, length, in);
		}
		st.unget();
		rec = getEmptyRecord(name, type, dclass, ttl, true);
		rec.rdataFromString(st, origin);
		t = st.get();
		if (t.type != Tokenizer.EOL && t.type != Tokenizer.EOF) {
			throw st.exception("unexpected tokens at end of record");
		}
		return rec;
	}

	/**
	 * Builds a new Record from its textual representation
	 * 
	 * @param name
	 *            The owner name of the record.
	 * @param type
	 *            The record's type.
	 * @param dclass
	 *            The record's class.
	 * @param ttl
	 *            The record's time to live.
	 * @param s
	 *            The textual representation of the rdata.
	 * @param origin
	 *            The default origin to be appended to relative domain names.
	 * @return The new record
	 * @throws IOException
	 *             The text format was invalid.
	 */
	public static Record fromString(Name name, int type, int dclass, long ttl,
			String s, Name origin) throws IOException {
		return fromString(name, type, dclass, ttl, new Tokenizer(s), origin);
	}

	/**
	 * Returns the record's name
	 * 
	 * @see Name
	 */
	public Name getName() {
		return name;
	}

	/**
	 * Returns the record's type
	 * 
	 * @see Type
	 */
	public int getType() {
		return type;
	}

	/**
	 * Returns the type of RRset that this record would belong to. For all types
	 * except RRSIG, this is equivalent to getType().
	 * 
	 * @return The type of record, if not RRSIG. If the type is RRSIG, the type
	 *         covered is returned.
	 * @see Type
	 * @see RRset
	 * @see SIGRecord
	 */
	public int getRRsetType() {
		if (type == Type.RRSIG) {
			RRSIGRecord sig = (RRSIGRecord) this;
			return sig.getTypeCovered();
		}
		return type;
	}

	/**
	 * Returns the record's class
	 */
	public int getDClass() {
		return dclass;
	}

	/**
	 * Returns the record's TTL
	 */
	public long getTTL() {
		return ttl;
	}

	/**
	 * Converts the type-specific RR to wire format - must be overriden
	 */
	abstract void rrToWire(DNSOutput out, Compression c, boolean canonical);

	/**
	 * Determines if two Records could be part of the same RRset. This compares
	 * the name, type, and class of the Records; the ttl and rdata are not
	 * compared.
	 */
	public boolean sameRRset(Record rec) {
		return (getRRsetType() == rec.getRRsetType() && dclass == rec.dclass && name
				.equals(rec.name));
	}

	/**
	 * Determines if two Records are identical. This compares the name, type,
	 * class, and rdata (with names canonicalized). The TTLs are not compared.
	 * 
	 * @param arg
	 *            The record to compare to
	 * @return true if the records are equal, false otherwise.
	 */
	public boolean equals(Object arg) {
		if (arg == null || !(arg instanceof Record))
			return false;
		Record r = (Record) arg;
		if (type != r.type || dclass != r.dclass || !name.equals(r.name))
			return false;
		byte[] array1 = rdataToWireCanonical();
		byte[] array2 = r.rdataToWireCanonical();
		return Arrays.equals(array1, array2);
	}

	/**
	 * Generates a hash code based on the Record's data.
	 */
	public int hashCode() {
		byte[] array = toWireCanonical(true);
		int code = 0;
		for (int i = 0; i < array.length; i++)
			code += ((code << 3) + (array[i] & 0xFF));
		return code;
	}

	Record cloneRecord() {
		try {
			return (Record) clone();
		} catch (CloneNotSupportedException e) {
			throw new IllegalStateException();
		}
	}

	/**
	 * Creates a new record identical to the current record, but with a
	 * different name. This is most useful for replacing the name of a wildcard
	 * record.
	 */
	public Record withName(Name name) {
		if (!name.isAbsolute())
			throw new RelativeNameException(name);
		Record rec = cloneRecord();
		rec.name = name;
		return rec;
	}

	/**
	 * Creates a new record identical to the current record, but with a
	 * different class and ttl. This is most useful for dynamic update.
	 */
	Record withDClass(int dclass, long ttl) {
		Record rec = cloneRecord();
		rec.dclass = dclass;
		rec.ttl = ttl;
		return rec;
	}

	/* Sets the TTL to the specified value. This is intentionally not public. */
	void setTTL(long ttl) {
		this.ttl = ttl;
	}

	/**
	 * Compares this Record to another Object.
	 * 
	 * @param o
	 *            The Object to be compared.
	 * @return The value 0 if the argument is a record equivalent to this
	 *         record; a value less than 0 if the argument is less than this
	 *         record in the canonical ordering, and a value greater than 0 if
	 *         the argument is greater than this record in the canonical
	 *         ordering. The canonical ordering is defined to compare by name,
	 *         class, type, and rdata.
	 * @throws ClassCastException
	 *             if the argument is not a Record.
	 */
	public int compareTo(Object o) {
		Record arg = (Record) o;

		if (this == arg)
			return (0);

		int n = name.compareTo(arg.name);
		if (n != 0)
			return (n);
		n = dclass - arg.dclass;
		if (n != 0)
			return (n);
		n = type - arg.type;
		if (n != 0)
			return (n);
		byte[] rdata1 = rdataToWireCanonical();
		byte[] rdata2 = arg.rdataToWireCanonical();
		for (int i = 0; i < rdata1.length && i < rdata2.length; i++) {
			n = (rdata1[i] & 0xFF) - (rdata2[i] & 0xFF);
			if (n != 0)
				return (n);
		}
		return (rdata1.length - rdata2.length);
	}

	/**
	 * Returns the name for which additional data processing should be done for
	 * this record. This can be used both for building responses and parsing
	 * responses.
	 * 
	 * @return The name to used for additional data processing, or null if this
	 *         record type does not require additional data processing.
	 */
	public Name getAdditionalName() {
		return null;
	}

	/* Checks that an int contains an unsigned 8 bit value */
	static int checkU8(String field, int val) {
		if (val < 0 || val > 0xFF)
			throw new IllegalArgumentException("\"" + field + "\" " + val
					+ " must be an unsigned 8 " + "bit value");
		return val;
	}

	/* Checks that an int contains an unsigned 16 bit value */
	static int checkU16(String field, int val) {
		if (val < 0 || val > 0xFFFF)
			throw new IllegalArgumentException("\"" + field + "\" " + val
					+ " must be an unsigned 16 " + "bit value");
		return val;
	}

	/* Checks that a long contains an unsigned 32 bit value */
	static long checkU32(String field, long val) {
		if (val < 0 || val > 0xFFFFFFFFL)
			throw new IllegalArgumentException("\"" + field + "\" " + val
					+ " must be an unsigned 32 " + "bit value");
		return val;
	}

	/* Checks that a name is absolute */
	static Name checkName(String field, Name name) {
		if (!name.isAbsolute())
			throw new RelativeNameException(name);
		return name;
	}

	static byte[] checkByteArrayLength(String field, byte[] array, int maxLength) {
		if (array.length > 0xFFFF)
			throw new IllegalArgumentException("\"" + field + "\" array "
					+ "must have no more than " + maxLength + " elements");
		byte[] out = new byte[array.length];
		System.arraycopy(array, 0, out, 0, array.length);
		return out;
	}

}
